﻿import pygame, os, time, thread, random
from pygame.locals import *
from Tkinter import *

# import modules such as task, queue, cpu from modules folder
path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../modules'))
if not path in sys.path:
  sys.path.insert(1, path)
del path

from constants import *
from task import Task
from queue import Queue
from vlist import List
from cpu import CPU
from simulator import *
from algorithmTask import AlgorithmTask

#####################################################
#                                                   #
#  CPUScheduler class:                              #
#                                                   # 
#  This class either simulates the CPU              #
#  scheduling algorithm or lets the user            #
#  interactively run the FIFO CPU scheduling        #
#                                                   #
#  Author: Felix Rohrer                             #     
#  Course: CS MET 575                               #
#  Year:   2011                                     #
#                                                   #
#####################################################

#class Plotter():
class CPUScheduler():
  def __init__(self, mainMenu, tasks=[]):
    # main menu object
    self.mainMenu = mainMenu
    self.frame = mainMenu.ctrlRegion
        
    # all tasks
    self.tasks = []

    # draw area color
    self.drawAreaColor = DRAWAREA_BG_COLOR

    # set default value for mode
    # mode = 1 -> simulating
    # mode = 0 -> interactive
    self.mode = IntVar()
    self.mode.set(1)

    # create algorithm plan for FIFO CPU Scheduling
    self.algorithmPlan = []
    task1 = AlgorithmTask('1. Either move BLOCKED task to READY \n   OR move READY task to CPU (random)', \
                          constraints={'BLOCKED':['READY'],'READY':['CPU']})
    task2 = AlgorithmTask('2. Move task from CPU to BLOCKED\n   (i.e. task has to wait for I/O)', \
                          constraints={'CPU':['BLOCKED']})

    # add the tasks to the algorithm plan
    self.algorithmPlan.append(task1)
    self.algorithmPlan.append(task2)

    # keep track of current algorithm task
    self.currentAlgTask = 0
    self.oldDraggedTaskPos = None

    # readyQueue holds all tasks initially
    # cpu is a one item queue
    self.ds = {'readyQueue': None, 'cpu': None,'blockedList': None}

    # simulator contains actions to be simulated
    self.sim = Simulator()

  # destroy the 'cpu simulation' control panel with all 
  # its dependent windows
  def destroy(self):

    # close the simulation window if opened
    self.stopSimulation()

    # do not show any information
    self.displayInfo(None)

    self.frame = None
    self.mainMenu = None

  # display information string in main window
  def displayInfo(self,infoMsg, pos=(0,10,5),color=ERR_BG_COLOR):
    if infoMsg is None:
      self.infoMsg.set('')
      self.infoMsgLabel.config(bg=MAINMENU_BG_COLOR,relief=FLAT)
    else:
      self.infoMsg.set(infoMsg)
      self.infoMsgLabel.config(bg=color,relief=GROOVE)
    self.infoMsgLabel.grid(column=pos[0],row=pos[1],columnspan=pos[2],sticky=W)

  # update mode (simulated or interactive CPU scheduling)
  def updateMode(self):
    if self.mode.get() == 1:
      self.speedScale.config(bg=MAINMENU_BG_COLOR)
      self.speedScale.config(state=NORMAL)
      self.speedLabel.config(state=NORMAL)
    else:
      self.speedScale.config(bg='grey')
      self.speedScale.config(state=DISABLED)
      self.speedLabel.config(state=DISABLED)

  # update current algorithm plan (go to next step)
  def updateAlgPlan(self, step):
    self.currentAlgTask = step
    self.algPlanLabel[(step-1)%len(self.algPlan)].config(bg=MAINMENU_BG_COLOR)
    self.algPlanLabel[step].config(bg=ALGPLAN_ACTIVE_COLOR)

  # every simulation needs to provide a controls function
  # the controls function will be called from the main menu, 
  # once this specific simulation is requested
  def controls(self):
    self.frame.grid()
    
    self.frame.grid_columnconfigure(0,minsize=150)

    # main labels
    self.cpuTitle = Label(self.frame, text="CPU Scheduling:", height=2,font=TITLE_FONT)
    self.infoTitle = Label(self.frame, text="Information:", height=2,font=TITLE_FONT)
    self.algPlanTitle = Label(self.frame, text="Algorithm plan:", height=2,font=TITLE_FONT)

    # label to display error messages
    self.algPlan = [StringVar(),StringVar()]
    self.algPlanLabel = []

    self.algPlan[0].set(self.algorithmPlan[0].getAlgText())
    self.algPlan[1].set(self.algorithmPlan[1].getAlgText())
    for l in range(len(self.algPlan)):
      self.algPlanLabel.append(Label(self.frame, textvariable=self.algPlan[l], padx=10, justify=LEFT, anchor=W, relief=GROOVE, width=32))

    self.infoMsg = StringVar()
    self.infoMsgLabel = Label(self.frame, textvariable=self.infoMsg, padx=10, justify=LEFT, anchor=W, relief = GROOVE, width=32)

    # start button
    self.controlLabel = Label(self.frame, text="Control CPU Scheduling: ", height=2,font=TITLE_FONT)
    self.startButton = Button(self.frame, text="Start simulation", command=self.startSimulation, width=15)
    self.stopButton = Button(self.frame, text="Stop simulation", command=self.stopSimulation, width=15,state=DISABLED)

    self.modeLabel = Label(self.frame, text="Select mode: ", height=2,font=TITLE_FONT)
    simulatingRadio = Radiobutton(self.frame, text="Simulate FIFO Scheduling", command=self.updateMode, variable=self.mode, value=1)
    simulatingRadio.select()
    interactiveRadio = Radiobutton(self.frame, text="Interactive FIFO Scheduling", command=self.updateMode, variable=self.mode, value=0, state=NORMAL)

    # simulation speed scale
    self.speedLabel = Label(self.frame, text="Simulation speed: ")
    self.speedScale = Scale(self.frame, from_=1,to=10, orient=HORIZONTAL, command=self.setSimSpeed)

    # Number of tasks scale
    self.taskLabel = Label(self.frame, text="Number of Tasks: ")
    self.taskScale = Scale(self.frame, from_=1,to=10, orient=HORIZONTAL, command=self.setNumTasks)

    # generate grid
    self.modeLabel.grid(column=0,row=0,sticky=W)
    simulatingRadio.grid(column=0,row=1,sticky=W)
    interactiveRadio.grid(column=0,row=2,sticky=W)
    self.cpuTitle.grid(column=0,row=3,sticky=W)
    self.taskLabel.grid(column=0, row=4, sticky=W)
    self.taskScale.grid(column=1, row=4, sticky=W)
    self.speedLabel.grid(column=0, row=5, sticky=W)
    self.speedScale.grid(column=1, row=5, sticky=W)
    self.controlLabel.grid(column=0,row=6,sticky=W)
    self.startButton.grid(column=0,row=7,sticky=W)
    self.stopButton.grid(column=0,row=8,sticky=W)

    # show information
    self.infoTitle.grid(columnspan=2,column=0,row=9,sticky=W)
 
    # show algorithm plan
    self.algPlanTitle.grid(columnspan=2,column=0,row=11,sticky=W)

    for l in range(len(self.algPlanLabel)):
      self.algPlanLabel[l].grid(column=0,row=12+l,columnspan=4,sticky=W)

    # display startup info
    self.displayInfo("Choose numbers of tasks and\nclick 'Start Simulation'. ",color=INFO_BG_COLOR)

  # set simulation speed
  def setSimSpeed(self,upd):
    self.simSpeed = int(upd)
    self.sim.updateSimSpeed(self.simSpeed)

  # set amount of numbers
  def setNumTasks(self,upd):
    self.numTasks = int(upd)

  # stop simulation
  def stopSimulation(self):
    self.going = False

  # every simulation needs this function in order to create
  # a new thread that will start the actual simulation window
  # A new thread is required to keep the main menu responsive
  def startSimulation(self):

    # only start new simulation if no other simulation running
    if not self.mainMenu.simulationRunning:

      # default task names
      taskNames = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

      # empty tasks list
      tasks = []

      # create required amount of tasks
      for namePtr in range(self.numTasks):
        tasks.append(Task(taskNames[namePtr]))

      # enable stop-button and disable start-button
      self.stopButton.config(state=NORMAL)
      self.startButton.config(state=DISABLED)

      # start new thread to run simulation
      # -> keeps main menu responsive
      thread.start_new_thread(self.plot,(tasks,self.mode.get(),))
      self.mainMenu.simulationRunning = True
    else:
      self.displayInfo("Error: another simulation already running")

  # returns False if task could not be removed
  def removeFromCurrentQueue(self, task):
    if task.getDSType() == 'QUEUE':
      if task.currentDS.peakNext() == task:
        task.currentDS.pop()
      else:
        # in a queue, only first element can be removed
        return False
    elif task.getDSType() == 'LIST':
      task.currentDS.remove(task)
    return True

  # check collision with any of the data structures
  def collisionWithDS(self, draggedTask, phase="None",interactive=False):
    currRect = Rect(draggedTask.pos[0],draggedTask.pos[1],draggedTask.size[0],draggedTask.size[1])
    found = False
    for v in self.ds.itervalues():
      listPos = currRect.collidelist(v.rects)
      # currently dragged task is colliding with data structure
      if listPos >= 0:
        # get data structure type (queue or list)
        dst = v.DSType

	# collision detected with this tasks current data structure
	# do not remove or add anything
	if draggedTask.currentDS is v:
	  found = True

        # only restrict possible collision, when in interactive mode
        if interactive:

          # check if algorithm plan constraints are met
          if not self.algorithmPlan[self.currentAlgTask].checkConstraint(fromDS=draggedTask.getDSName(), toDS = v.name):
            # algorithm task constraint not fulfilled
            self.displayInfo("ERROR: This algorithm does not allow \n"+" "*12+draggedTask.getDSName()+' -> '+v.name)
            if self.oldDraggedTaskPos is not None:
	      draggedTask.setPos(self.oldDraggedTaskPos)
	    return

          # reset old information
          self.displayInfo(None)

        # if currently dragged task can be removed from its
        # old data structure (or does not belong to a data structure)
        # add the currently dragged task to this new data structure
        if not found:
          removed = self.removeFromCurrentQueue(draggedTask)
	  if removed and phase is not 'EXEC':
	    if dst == 'QUEUE':
	      if not v.push(draggedTask):
                self.displayInfo("ERROR: Could not add task to "+v.name)
	      found = True
	    elif dst == 'LIST':
	      v.insert(draggedTask,listPos)
	      found = True
    # if currently dragged task did not collide with 
    # any data structure, the task is on a non-data structure
    # field and its current data structure should be removed
    if not found:
      if interactive:
        draggedTask.setPos(self.oldDraggedTaskPos)
      else:
        self.removeFromCurrentQueue(draggedTask)

  # plot the graph and all additional buttons and input fields 
  def plot(self,tasks=[],simulate=False):

    # create clock to limit FPS
    clock = pygame.time.Clock()

    os.environ['SDL_VIDEO_WINDOW_POS'] = DRAWAREA_INIT_POS
    # simulation speed
    if self.mainMenu is None:
      self.simSpeed = 5
    else:
      self.simSpeed = self.speedScale.get()

    self.tasks = tasks

    # initialize simulation window
    pygame.init()
    pygame.display.init()

    self.going = True
    self.screenDim = {'width': DRAWAREA_WIDTH, 'height': DRAWAREA_HEIGHT}

    self.screen = pygame.display.set_mode ((self.screenDim['width'], self.screenDim['height']))
    pygame.display.set_caption("CPU Scheduling")

    # This list holds the BLOCKED tasks
    self.ds['blockedList'] = List(len(self.tasks), pos=(self.screenDim['width']-(len(self.tasks)+1)*50,200), name='BLOCKED')

    # This queue holds the READY tasks
    self.ds['readyQueue'] = Queue(len(self.tasks), pos=(20,20), name='READY')

    # create CPU
    self.ds['cpu'] = CPU(pos=(self.screenDim['width']/2-25,self.screenDim['height']*3/4))

    background = pygame.Surface(self.screen.get_size())
    background = background.convert()

    # drawing area
    drawSize = self.screenDim['width'],self.screenDim['height']
    drawArea = pygame.Surface(drawSize)
    drawArea.fill(self.drawAreaColor)

    # define font
    textFont = pygame.font.SysFont(SYS_FONT,SYS_FONT_SIZE)
    textFont.set_bold(True)

    # all tasks are in the beginning inside the list
    for t in self.tasks:
      if not self.ds['readyQueue'].push(t):
        self.displayInfo("ERROR: Could not add task to readyQueue")

    # signal a mouse click and hold event
    mouseDown = False
    draggedTask = None

    while self.going:
      # limit frame rate
      time_passed = clock.tick(FPS)

      event = pygame.event.poll()
      if event.type == QUIT:
        self.going = False
      if event.type == KEYDOWN:
        if event.key == K_ESCAPE:
          self.going = False
      if event.type == MOUSEBUTTONDOWN:
        # check if left mouse pressed down
        if pygame.mouse.get_pressed()[0] == True:
          if self.mode.get() == 1:
            self.displayInfo("Error: No user interaction allowed \nduring simulation. ")
          else:
            # left mouse clicked and held
            mouseDown = True
      # mouse released
      if event.type == MOUSEBUTTONUP:
        if self.mode.get() == 1:
          self.displayInfo(None)
        # mouse click released
        mouseDown = False
       
        # stop moving any previously moved task
        if draggedTask is not None:

          # if current task is not allowed to move
          # an error message was written when this task
          # was picked up. 
          # The task is dropped here, so the error message
          # has to be removed
          if draggedTask.getCondition() == 'NOMOVE':
            self.displayInfo(None)

          draggedTask.dropped()

          # check for collision with queues (ready queue and CPU)
          self.collisionWithDS(draggedTask, interactive = (not simulate))

          self.oldDraggedTaskPos = None
          draggedTask = None

      # draw graph
      self.screen.blit(background, (0, 0))

      # draw drawing area
      self.screen.blit(drawArea, (0,0))

      # update the drawing area
      drawArea.fill(self.drawAreaColor)
      pygame.draw.rect(drawArea, (0,0,0), (0,0,drawSize[0],drawSize[1]),1)

      # show lists and queues
      for d in self.ds.itervalues():
        d.display(drawArea) 

      # update currently dragged tasks position
      if mouseDown:
        newPos = pygame.mouse.get_pos()

        # check if mouse click collides with any task or 
        # any task is currently already selected to be moved
        if draggedTask is None:
	  for t in self.tasks:
	    if t.collision(newPos):
	      draggedTask = t

              # if the current task is in a queue, only allow it to be
              # moved if the task is at the queues head
              if draggedTask.getDSType() == 'QUEUE':
                if draggedTask == draggedTask.currentDS.peakNext():
                  self.oldDraggedTaskPos = list(draggedTask.getPos())
                  draggedTask.setCondition('DRAGGED')
                else:
                  self.displayInfo("ERROR: This task can currently not\n"+' '*13+"be removed from this queue. ")
                  draggedTask.setCondition('NOMOVE')
              # this dragged tasks data structure is a list
              else:
                self.oldDraggedTaskPos = list(draggedTask.getPos())
                draggedTask.setCondition('DRAGGED')
              break 

        # there is already a currently dragged task
        # therefore, simply update its position according
        # to the mouse position
        else:
          oldPos = draggedTask.getPos(offset=True)
	  # update tasks position if different from previous position
          if not newPos[0] == oldPos[0] or not newPos[1] == oldPos[1]:
	    draggedTask.setPos(newPos,True)

      # draw all tasks
      for t in self.tasks:
        if not t == draggedTask:
          t.display(drawArea)

      # this ensures, that the currently dragged
      # task will always be displayed last
      # -> will be drawn on top of other objects
      if draggedTask is not None:
        draggedTask.display(drawArea)

      # if simulation enabled, run simulation here
      if simulate: 
        if not self.sim.executeStep():
          # only add new step, if CPU is available
          if self.ds['cpu'].isEmpty():
            readyEmpty = self.ds['readyQueue'].isEmpty()
            blockedEmpty = self.ds['blockedList'].isEmpty()

            if not readyEmpty and not blockedEmpty:
              # randomly move a task from blockedList to readyQueue or from readyQueue to CPU
              rand = random.randint(0,1)
 
              # either move a task from blockedList to readyQueue
              # or move a task from readyQueue to CPU
              if rand == 0:
                # choose a random task from list
                # if only ready tasks are taken into consideration, 
                # randomTask may return no task at all. In this case, do nothing
                randomTask = self.ds['blockedList'].randomTask(onlyReadyTasks=True)
                if randomTask:
                  self.updateAlgPlan(0)
	          self.sim.addStep('MOVE', randomTask, toPos = self.ds['readyQueue'].getTailPos())
              else:
                self.updateAlgPlan(0)
	        self.sim.addStep('MOVE', self.ds['readyQueue'].tasks[0], toPos = self.ds['cpu'].getPos())

            # if tasks are waiting for CPU time, get next task (FIFO)
	    elif not readyEmpty:
              self.updateAlgPlan(0)
	      self.sim.addStep('MOVE', self.ds['readyQueue'].tasks[0], toPos = self.ds['cpu'].getPos())

            # if tasks are not waiting but some tasks are blocked, unblock them
	    elif not blockedEmpty:
	      # choose a random task from list
	      # if only ready tasks are taken into consideration, 
	      # randomTask may return no task at all. In this case, do nothing
	      randomTask = self.ds['blockedList'].randomTask(onlyReadyTasks=True)
	      if randomTask:
                self.updateAlgPlan(0)
		self.sim.addStep('MOVE', randomTask, toPos = self.ds['readyQueue'].getTailPos())

          # move current task from CPU to a queue
          else:
	    self.sim.addStep('MOVE', self.ds['cpu'].tasks[0], toPos = self.ds['blockedList'].getHeadPos())
            self.updateAlgPlan(1)

            #if self.ds['cpu'].tasks[0].getExecTime() > 0:
            #  self.updateAlgPlan(1)
            
	  # update speed according to tkinter scale value
	  self.sim.updateSimSpeed(self.simSpeed)
        currPhase = self.sim.getActiveObjectPhase()
        # if there is still something to simulate
        # get the involved object and check if there
        # are any collisions between that object and any 
        # data structure
        if currPhase:
	  obj = self.sim.getActiveObject()
          self.collisionWithDS(obj, phase=currPhase, interactive = (not simulate))
      # interactive
      else:
        if self.ds['cpu'].isEmpty():
          self.updateAlgPlan(0)
        else:
          self.updateAlgPlan(1)

      pygame.display.flip()

    # this simulation could run stand-alone, without a main menu gui
    self.stopButton.config(state=DISABLED)
    self.startButton.config(state=NORMAL)
    self.displayInfo("Stopping simulation...")
    self.frame.update_idletasks()

    # reset algorithm plan color
    for l in self.algPlanLabel:
      l.config(bg=MAINMENU_BG_COLOR)

    # restore task counter to 0
    Task.TASK_COUNT = 0

    # uninitialize queue object
    for v in self.ds.itervalues():
      v.empty()
    # remove all data structures from ds container
    self.ds.clear()
    if simulate:
      self.sim.empty()
    del self.tasks[:]
    pygame.quit ()
    self.displayInfo("Simulation stopped. ", color=INFO_BG_COLOR)
    self.mainMenu.simulationRunning = False
